<!DOCTYPE html>


<html lang="zh-CN">


<head>
  <meta charset="utf-8" />
  <meta name="baidu-site-verification" content="code-kg5UjKJZM2" />
   
  <meta name="keywords" content="活,炼" />
   
  <meta name="description" content="shimmerjordan" />
  
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
  <title>
    AssetBundle浅析 |  丛烨-shimmerjordan
  </title>
  <meta name="generator" content="hexo-theme-ayer">
  
  <link rel="shortcut icon" href="/favicon.ico" />
  
  
<link rel="stylesheet" href="/dist/main.css">

  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Shen-Yu/cdn/css/remixicon.min.css">
  
<link rel="stylesheet" href="/css/custom.css">

  
  <script src="https://cdn.jsdelivr.net/npm/pace-js@1.0.2/pace.min.js"></script>
  
  

<script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'G-Q0DT8B8VJW', 'auto');
ga('send', 'pageview');

</script>



  
<script>
var _hmt = _hmt || [];
(function() {
	var hm = document.createElement("script");
	hm.src = "https://hm.baidu.com/hm.js?6d06f826e125297d4ce0fa7a1449328e";
	var s = document.getElementsByTagName("script")[0]; 
	s.parentNode.insertBefore(hm, s);
})();
</script>


<link rel="alternate" href="/atom.xml" title="丛烨-shimmerjordan" type="application/atom+xml">
</head>

</html>

	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome/css/font-awesome.min.css">
	<script src="https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/autoload.js"></script>


<body>
  <div id="app">
    
      
    <main class="content on">
      <section class="outer">
  <article
  id="post-AssetBundle浅析"
  class="article article-type-post"
  itemscope
  itemprop="blogPost"
  data-scroll-reveal
>
  <div class="article-inner">
    
    <header class="article-header">
       
<h1 class="article-title sea-center" style="border-left:0" itemprop="name">
  AssetBundle浅析
</h1>
 

    </header>
     
    <div class="article-meta">
      <a href="/2024/08/23/assetbundle-intro/" class="article-date">
  <time datetime="2024-08-23T11:30:12.000Z" itemprop="datePublished">2024-08-23</time>
</a> 
  <div class="article-category">
    <a class="article-category-link" href="/categories/Unity/">Unity</a>
  </div>
  
<div class="word_count">
    <span class="post-time">
        <span class="post-meta-item-icon">
            <i class="ri-quill-pen-line"></i>
            <span class="post-meta-item-text"> 字数统计:</span>
            <span class="post-count">7.6k</span>
        </span>
    </span>

    <span class="post-time">
        &nbsp; | &nbsp;
        <span class="post-meta-item-icon">
            <i class="ri-book-open-line"></i>
            <span class="post-meta-item-text"> 阅读时长≈</span>
            <span class="post-count">27 分钟</span>
        </span>
    </span>
</div>
 
    </div>
      
    <div class="tocbot"></div>




  
    <div class="article-entry" itemprop="articleBody">
       
  <p><strong>AssetBundle系统提供了一种压缩文件的格式，可以把一个到多个文件进行索引和序列化。</strong></p>
<p>Unity项目在交付安装之后，会通过AssetBundle对不包含代码的资源进行更新。这就允许开发人员先提交一个小的应用程序包，将运行时内存压力降到最低，并有选择地加载针对不同终端用户设备优化后的内容。</p>
<span id="more"></span>
<h1 id="AssetBundle原理"><a href="#AssetBundle原理" class="headerlink" title="AssetBundle原理"></a>AssetBundle原理</h1><h2 id="AssetBundle结构"><a href="#AssetBundle结构" class="headerlink" title="AssetBundle结构"></a>AssetBundle结构</h2><p>总的来说，AssetBundle就像传统的压缩包一样，由两个部分组成：包头和数据段。</p>
<p>包头包含有关AssetBundle的信息，比如标识符、压缩类型和内容清单。清单是一个以Objects name为键的查找表。每个条目都提供一个字节索引，用来指示该Objects在AssetBundle数据段的位置。在大多数平台上，这个查找表是用平衡搜索树实现的。具体来说，Windows和OSX派生平台（包括iOS）都采用了红黑树。因此，构建清单所需的时间会随着AssetBundle中Assets的数量增加而线性增加。</p>
<p>数据段包含通过序列化AssetBundle中的Assets而生成的原始数据。如果指定LZMA为压缩方案，则对所有序列化Assets后的完整字节数组进行压缩。如果指定了LZ4，则单独压缩单独Assets的字节。如果不使用压缩，数据段将保持为原始字节流。</p>
<blockquote>
<p>在Unity 5.3之前，是无法对AssetBundle中单独Objects进行压缩的。因此，如果在5.3之前的Unity版本被要求从压缩的AssetBundle中读取一个或多个对象时，Unity必须解压整个AssetBundle。通常，Unity会缓存AssetBundle的解压缩副本，以提高在相同AssetBundle上的后续加载请求的加载性能。</p>
</blockquote>
<h2 id="加载AssetBundle"><a href="#加载AssetBundle" class="headerlink" title="加载AssetBundle"></a>加载AssetBundle</h2><p>AssetBundles可以通过四个不同的API进行加载。但受限于两个标准，这四个API的行为是不同的。2个标准如下：</p>
<ul>
<li>AssetBundles的压缩方式：LZMA、LZ4、还是未压缩的。</li>
<li>AssetBundles的加载平台。</li>
</ul>
<p>而四个API分别是：</p>
<ul>
<li><code>AssetBundle.LoadFromMemory(Async optional)</code></li>
<li><code>AssetBundle.LoadFromFile(Async optional)</code></li>
<li>UnityWebRequest’s <code>DownloadHandlerAssetBundle</code></li>
<li><code>WWW.LoadFromCacheOrDownload</code> (on Unity 5.6 or older)</li>
</ul>
<h3 id="4个API的区别"><a href="#4个API的区别" class="headerlink" title="4个API的区别"></a>4个API的区别</h3><p><strong>AssetBundle.LoadFromMemory(Async)</strong><br>Unity的建议是——<strong>不要使用这个API</strong>。</p>
<p><code>LoadFromMemory(Async)</code> 是从托管代码的字节数组里加载AssetBundle。也就是说你要提前用其它的方式将资源的二进制数组加入到内存中。然后该接口会将源数据从托管代码字节数组复制到新分配的、连续的本机内存块中。</p>
<p>但如果AssetBundle使用了LZMA压缩类型，它将在复制时解压AssetBundle。而未压缩和LZ4压缩类型的AssetBundle将逐字节的完整复制。</p>
<p>之所以不建议使用该API是因为，此API消耗的最大内存量将至少是AssetBundle的两倍：本机内存中的一个副本，和<code>LoadFromMemory(Async)</code>从托管字节数组中复制的一个副本。</p>
<p>因此，从通过此API创建的AssetBundle加载的资产将在内存中冗余三次：一次在托管代码字节数组中，一次在AssetBundle的栈内存副本中，第三次在GPU或系统内存中，用于Asset本身。</p>
<p>注意：在Unity 5.3.3之前，这个API被称为<code>AssetBundle.CreateFromMemory</code>。但功能没有改变。</p>
<p><strong>AssetBundle.LoadFromFile(Async)</strong><br>LoadFromFile是一种高效的API，用于从本地存储（如硬盘或SD卡）加载未压缩或LZ4压缩格式的AssetBundle。</p>
<p>在桌面独立平台、控制台和移动平台上，API将只加载AssetBundle的头部，并将剩余的数据留在磁盘上。</p>
<p>AssetBundle的Objects会按需加载，比如：加载方法（例如：AssetBundle.Load）被调用或其InstanceID被间接引用的时候。在这种情况下，不会消耗过多的内存。</p>
<p>但在Editor环境下，API还是会把整个AssetBundle加载到内存中，就像读取磁盘上的字节和使用<code>AssetBundle.LoadFromMemoryAsync</code>一样。</p>
<p>如果在Editor中对项目进行了分析，此API可能会导致在AssetBundle加载期间出现内存尖峰。但这不应影响设备上的性能，在做优化之前，这些尖峰应该在设备上重新再测试一遍。</p>
<p>要注意，这个API只针对未压缩或LZ4压缩格式，因为前面说过了，如果使用LZMA压缩，它是针对整个生成后的数据包进行压缩的，所以在未解压之前是无法拿到AssetBundle的头信息的。</p>
<p>注意：这里曾经有过一个历史遗留问题，即在Unity 5.3或更老版本的Android设备上，当试图从Streaming Assets路径加载AssetBundles时，此API将失败。这个问题已在Unity 5.4中解决。</p>
<p>在Unity 5.3之前，这个API被称为<code>AssetBundle.CreateFromFile</code>。其功能没有改变。</p>
<p><strong>AssetBundleDownloadHandler</strong><br><code>DownloadHandlerAssetBundle</code>的操作是通过UnityWebRequest的API来完成的。</p>
<p>UnityWebRequest API允许开发人员精确地指定Unity应如何处理下载的数据，并允许开发人员消除不必要的内存使用。使用UnityWebRequest下载AssetBundle的最简单方法是调用<code>UnityWebRequest.GetAssetBundle</code>。</p>
<p>就实战项目而言，最有意思的类是<code>DownloadHandlerAssetBundle</code>。它使用工作线程，将下载的数据流存储到一个固定大小的缓冲区中，然后根据下载处理程序的配置方式将缓冲数据放到临时存储或AssetBundle缓存中。</p>
<p>所有这些操作都发生在非托管代码中，消除了增加堆内存的风险。此外，该下载处理程序并不会保留所有下载字节的栈内存副本，从而进一步减少了下载AssetBundle的内存开销。</p>
<p>LZMA压缩的AssetBundles将在下载和缓存的时候更改为LZ4压缩。这个可以通过设置<code>Caching.CompressionEnable</code>属性来更改。</p>
<p>如果将缓存信息提供给UnityWebRequest对象，一旦有请求的AssetBundle已经存在于Unity的缓存中，那么AssetBundle将立即可用，并且此API的行为将会与<code>AssetBundle.LoadFromFile</code>相同操作。</p>
<p>在Unity 5.6之前，UnityWebRequest系统使用了一个固定的工作线程池和一个内部作业系统来防止过多的并发下载，并且线程池的大小是不可配置的。在Unity 5.6中，这些安全措施已经被删除，以便适应更现代化的硬件，并允许更快地访问HTTP响应代码和报头。</p>
<p><strong>WWW.LoadFromCacheOrDownload</strong><br>这是一个很古老的API了，从Unity 2017.1开始，就只是简单地包装了UnityWebRequest。因此，使用Unity 2017.1或更高版本的开发者应该直接使用UnityWebRequest来工作。Unity已经放弃了对改接口的维护，并可能在未来的某个版本中移除。</p>
<p>所以下面说的这些内容只适合于Unity 5.6或更老的版本。</p>
<p><code>WWW.LoadFromCacheOrDownload</code>允许从远程服务器和本地存储加载对象。也可以通过文件URL从本地存储加载文件。如果AssetBundle存在于Unity Cache中，则此API的行为将与<code>AssetBundle.LoadFromFile</code>完全相同。</p>
<p>如果AssetBundle尚未缓存，则<code>WWW.LoadFromCacheOrDownload</code>会将从它的源文件读取AssetBundle。如果AssetBundle被压缩过，它会使用工作线程进行解压缩并写入缓存中。否则，它将通过工作线程直接写入缓存。</p>
<p>在缓存AssetBundle之后，<code>WWW.LoadFromCacheOrDownload</code>将从缓存的、解压缩的AssetBundle加载头信息。然后，和<code>AssetBundle.LoadFromFile</code>加载AssetBundle行为相同。</p>
<p>此缓存会在<code>WWW.LoadFromCacheOrDownload</code>和UnityWebRequest之间共享。一个API下载的任何AssetBundle也可以通过另一个API获得。</p>
<p>虽然数据将通过固定大小的缓冲区解压缩并写入缓存，但WWW对象会在本机内存中保留AssetBundle字节的完整副本。这个额外副本被保留的原因是因为要支持WWW.bytes字节属性。</p>
<p>由于在WWW对象中缓存AssetBundle的字节的内存开销，所以，实际项目开发中AssetBundles应该要保持较少的体积以便减少内存。</p>
<p>与UnityWebRequest不同的是，每次调用这个API都会产生一个新的工作线程。因此，在手机等内存有限的平台上，最好限定一次只能下载一个AssetBundle，以避免内存激增。而在其它平台也要小心创建过多的线程。如果需要下载5个以上的AssetBundles，建议在脚本代码中创建和管理下载队列，以确保只有少数几个AssetBundle同时下载。</p>
<p><strong>建议</strong></p>
<ol>
<li>一般来说，只要有可能，就应该使用<code>AssetBundle.LoadFromFile</code>。这个API在速度、磁盘使用和运行时内存使用方面是最有效的。</li>
<li>对于必须下载或热更新AssetBundles的项目，强烈建议对使用Unity 5.3或更高版本的项目使用UnityWebRequest，对于使用Unity 5.2或更老版本的项目使用<code>WWW.LoadFromCacheOrDownload</code>。</li>
<li>当使用UnityWebRequest或<code>WWW.LoadFromCacheOrDownload</code>时，要确保下载程序代码在加载AssetBundle后正确地调用Dispose。另外，C#的using语句是确保WWW或UnityWebRequest被安全处理的最方便的方法。</li>
<li>对于需要独特的、特定的缓存或下载需求的大项目，可以考虑使用自定义的下载器。编写自定义下载程序是一项重要并且复杂的任务，任何自定义的下载程序都应该与<code>AssetBundle.LoadFromFile</code>保持兼容。</li>
</ol>
<h2 id="从AssetBundle中加载Assets"><a href="#从AssetBundle中加载Assets" class="headerlink" title="从AssetBundle中加载Assets"></a>从AssetBundle中加载Assets</h2><p>到这里，我们已经能够获得AssetBundles了，那么接下来就是要从AssetBundles里获取Assets。</p>
<p>Unity提供了三个不同的API从AssetBundles加载<code>UnityEngine.Objects</code>，这些API都绑定到AssetBundle对象上，并且这些API具有同步和异步变体：</p>
<ul>
<li><code>LoadAsset (LoadAssetAsync)</code></li>
<li><code>LoadAllAssets (LoadAllAssetsAsync)</code></li>
<li><code>LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)</code></li>
</ul>
<p>并且这些API的同步版本总是比异步版本快至少一个帧（其实是因为异步版本为了确保异步，都至少延迟了1帧），异步加载每帧会加载多个对象，直到它们的时间切片切出。</p>
<p>加载多个独立的<code>UnityEngine.Objects</code>时应使用LoadAllAsset。并且只有在需要加载AssetBundle中的大多数或所有对象时，才应该使用它。与其它两个API相比，LoadAllAsset比对LoadAsset的多个单独调用略快一些。因此，如果要加载的Asset数量很大，但如果需要一次性加载不到三分之二的AssetBundle，则要考虑将AssetBundle拆分为多个较小的包，再使用LoadAllAsset。</p>
<p>加载包含多个嵌入式对象的复合Asset时，应使用LoadAssetWithSubAsset，例如嵌入动画的FBX模型或嵌入多个精灵的sprite图集。也就是说，如果需要加载的对象都来自同一Asset，但与许多其它无关对象一起存储在AssetBundle中，则使用此API。</p>
<p>任何其它情况，请使用LoadAsset或LoadAssetAsync。</p>
<h3 id="低层级的加载细节"><a href="#低层级的加载细节" class="headerlink" title="低层级的加载细节"></a>低层级的加载细节</h3><p>Object加载是在主线程上执行，但数据从工作线程上的存储中读取。任何不触碰Unity系统中线程敏感部分（脚本、图形）的工作都将在工作线程上转换。例如，VBO将从网格创建，纹理将被解压等等。</p>
<p>从Unity 5.3开始，Object加载就被并行化了。在工作线程上反序列化、处理和集成多个Object。当一个Object完成加载时，它的Awake回调将被调用，该对象的其余部分将在下一个帧中对UnityEngine可用。</p>
<p>同步<code>AssetBundle.Load</code>方法将暂停主线程，直到Object加载完成。但它们也会加载时间切片的Object，以便Object集成不会占用太多的毫秒帧时间。应用程序属性设置毫秒数的属性为<code>Application.backgroundLoadingPriority</code>。  </p>
<ul>
<li><code>ThreadPriority.High</code>：每帧最多50毫秒  </li>
<li><code>ThreadPriority.Normal</code>：每帧最多10毫秒</li>
<li><code>ThreadPriority.BelowNormal</code>：每帧最多4毫秒  </li>
<li><code>ThreadPriority.Low</code>：每帧最多2毫秒</li>
</ul>
<p>从Unity 5.2开始，加载多个对象的时候，会一直进行直到达到对象加载的帧时间限制为止。假设所有其它因素相等，Asset加载API的异步变体将总是比同步版本花费更长的时间，因为发出异步调用和对象之间有最小的一帧延迟。</p>
<h3 id="AssetBundle依赖项"><a href="#AssetBundle依赖项" class="headerlink" title="AssetBundle依赖项"></a>AssetBundle依赖项</h3><p>根据运行时环境的不同，使用两个不同的API自动跟踪AssetBundles之间的依赖关系。在UnityEditor中，可以通过AssetDatabaseAPI查询AssetBundle依赖项。AssetBundles分配和依赖项可以通过AssetImportAPI访问和更改。在运行时，Unity提供了一个可选的API，通过基于ScriptableObject的AssetBundleManifest API加载在AssetBundle构建过程中生成的依赖信息。</p>
<p>当一个或多个AssetBundle的UnityEngine.Objects引用了一个或者多个其它AssetBundle的UnityEngine.Objects，那么这个AssetBundle就会依赖于另外的AssetBundle。AssetBundles充当由它包含的每个对象的FileGUID和LocalID标识的源数据。</p>
<p>因为一个对象是在其Instance ID第一次被间接引用时加载的，而且由于一个对象在加载其AssetBundle时被分配了一个有效的Instance ID，所以加载AssetBundles的顺序并不重要。相反，在加载对象本身之前，重要的是加载包含对象依赖关系的所有AssetBundles。Unity不会尝试在加载父AssetBundle时自动加载任何子AssetBundle。</p>
<p><strong>示例</strong><br>假设Material A引用Texture B。Material A被打包到AssetBundle1中，Texture B被打包到AssetBundle2中：<br><img src="https://gcore.jsdelivr.net/gh/shimmerjordan/pic_bed@obsidian-assets/AssetBundle浅析/image-20240825190138057.png" alt="github pic"><br>在本用例中，AssetBundle2必须在Material A从AssetBundle1中加载之前先加载。这并不意味着AssetBundle 2必须在AssetBundle 1之前加载，或者Texture B必须从AssetBundle 2中显式加载。在将Material A从AssetBundle 1加载之前加载AssetBundle 2就足够了。</p>
<p>简单来说就是AssetBundle之间的加载没有先后，但是Asset的加载有。</p>
<p>有关AssetBundle依赖项的详细信息，请<a target="_blank" rel="noopener" href="https://docs.unity3d.com/Manual/AssetBundles-Dependencies.html?_ga=2.168691873.1408835506.1571651191-1030292064.1564583003">参阅手册页</a>。</p>
<h3 id="AssetBundle-manifests"><a href="#AssetBundle-manifests" class="headerlink" title="AssetBundle manifests"></a>AssetBundle manifests</h3><p>当使用<code>BuildPiine.BuildAssetBundles</code> API执行AssetBundle构建管线时，Unity会序列化一个包含每个AssetBundle依赖项信息的对象。此数据存储在单独的AssetBundle中，其中包含AssetBundleManifest类型的单个对象。</p>
<p>此Asset将存储在与构建AssetBundles的父目录同名的AssetBundle中。如果一个项目将其AssetBundles构建到位于<code>(Projectroot)/Build/Client/</code>的文件夹中，那么包含清单的AssetBundle将被保存为<code>(Projectroot)/build/client/Client.manifest</code>。</p>
<p>包含Manifest的AssetBundle可以像任何其它AssetBundle一样加载、缓存和卸载。</p>
<p>AssetBundleManifest对象本身提供GetAllAssetBundles API来列出与清单同时构建的所有AssetBundles，以及查询特定AssetBundle的依赖项的两个方法：</p>
<ul>
<li><code>AssetBundleManifest.GetAllDependencies</code>返回AssetBundle的所有层次依赖项，其中包括AssetBundle的直接子级、其子级的依赖项等。</li>
<li><code>AssetBundleManifest.GetDirectDependations</code>只返回AssetBundle的直接子级</li>
</ul>
<p>请注意，这两个API分配的都是字符串数组。因此，最好是在性能要求不敏感的时候使用。</p>
<p><strong>建议</strong><br>在多数情况下，最好在玩家进入应用程序的性能关键区域（如主游戏关卡或世界）之前加载尽可能多的所需对象。这在移动平台上尤为重要，因为在移动平台上，访问本地存储的速度很慢，并且在运行时加载和卸载对象会触发垃圾回收。</p>
<h1 id="AssetBundle实践"><a href="#AssetBundle实践" class="headerlink" title="AssetBundle实践"></a>AssetBundle实践</h1><p>这一节讨论下在实际使用AssetBundles的过程中遇到的问题以及解决方案。</p>
<h2 id="管理已经加载的Assets"><a href="#管理已经加载的Assets" class="headerlink" title="管理已经加载的Assets"></a>管理已经加载的Assets</h2><p>在一些内存敏感的环境中，严格控制加载对象的大小和数量是至关重要的。当对象从活动场景中被移除时，Unity并不会自动卸载对象。Assets的清理工作是在特定时间触发的，当然也可以手动触发（其实就是GC了）。</p>
<p>AssetBundle必须要小心管理。在本地存储（通过Unity缓存或通过<code>AssetBundle.LoadFromFile</code>加载的文件）里的文件支持的AssetBundle具有最小的内存开销，很少超过几十K字节。但是，如果存在大量的AssetBundles，这种开销仍然会成为问题。</p>
<p>由于大多数项目允许用户重复体验游戏内容（例如重新打一个关卡），因此了解何时加载或卸载AssetBundle就变得非常重要。如果AssetBundle卸载不当，则会导致内存中的对象产生冗余副本。而在某些情况下，不适当地卸载AssetBundles也会导致不良行为，例如纹理丢失。</p>
<p>在管理Asset和AssetBundle时，最重要的一点是调用<code>AssetBundle.unload</code>时的方式，unload参数为true或false。</p>
<p>此API将卸载正在调用的AssetBundle的包头信息。unload参数决定是否也卸载从此AssetBundle实例化的所有对象。如果设置为true，那么从AssetBundle创建的所有对象也将立即卸载！即使它们目前正在活动场景中被引用。</p>
<p>举个例子，假设Material M是从AssetBundle AB加载的，并且假设M当前在活动场景中。<br><img src="https://gcore.jsdelivr.net/gh/shimmerjordan/pic_bed@obsidian-assets/AssetBundle浅析/image-20240825190831764.png" alt="github pic"></p>
<p>如果调用了<code>AssetBundle.Unload(True)</code>，则M将从场景中移除，销毁并卸载。但是，如果调用<code>AssetBundle.Unload(False)</code>，则AB的包头信息将被卸载，但M将保持在场景中，并且仍然是可用的。调用<code>AssetBundle.Unload(False)</code>破坏了M和AB之间的链接。如果AB稍后再次加载，则AB中包含的对象的新副本将会被加载到内存中。<br><img src="https://gcore.jsdelivr.net/gh/shimmerjordan/pic_bed@obsidian-assets/AssetBundle浅析/image-20240825190855794.png" alt="github pic"><br>如果AB稍后再次加载，将会重新加载AssetBundle的头信息的新副本。然而，M不是从这个新的AB拷贝加载的。Unity并没有在AB和M的新副本之间建立任何联系。<br><img src="https://gcore.jsdelivr.net/gh/shimmerjordan/pic_bed@obsidian-assets/AssetBundle浅析/image-20240825190909336.png" alt="github pic"><br>如果调用<code>AssetBundle.LoadAsset()</code>来重新加载M，Unity将不会将旧的M副本作为为AB中数据的实例。因此，Unity将加载一个新的副本的M，所以此时将会有两个相同的副本M在现场。<br><img src="https://gcore.jsdelivr.net/gh/shimmerjordan/pic_bed@obsidian-assets/AssetBundle浅析/image-20240825190920372.png" alt="github pic"><br>对于大多数项目来说，这样的结果是不可取的。大多数项目应该使用<code>AssetBundle.Unload(True)</code>，并采用一种方法来确保对象不被复制。两种常见的方法是：  </p>
<ol>
<li>在应用程序的生命周期内定义一个合适的节点，并在此期间卸载不需要的AssetBundle，例如在关卡切换或加载屏幕期间。这是最简单和最常见的选择。</li>
<li>维护单个对象的引用计数，并仅当所有组成对象都未使用时才卸载AssetBundles。这允许应用程序在不重复内存的情况下卸载和重新加载单个对象。</li>
</ol>
<p>如果应用程序必须使用<code>AssetBundle.Unload(False)</code>，那么只能通过两种方式卸载各个对象：  </p>
<ol>
<li>在场景和代码中消除对不需要的对象的所有引用。完成后，调用<code>Resources.UnusedAsset</code>。</li>
<li>非附加加载场景。这将销毁当前场景中的所有对象并调用<code>Resources.UnusedAsset</code>。</li>
</ol>
<p>如果项目有明确定义的节点，玩家可以等待对象加载和卸载，例如在游戏模式或关卡之间切换，则可以根据需要卸载尽可能多的对象和加载新对象。</p>
<p>最简单的方法是将项目的离散块打包到场景中，然后将这些场景与所有依赖项一起构建到AssetBundles中。然后，应用程序可以切换到“loading”场景，从而完全卸载包含旧场景的AssetBundles，然后加载包含新场景的AssetBundles。</p>
<p>虽然这是最简单的流程，但却需要很复杂的AssetBundles管理。由于每个项目是不同的，Unity尚没有提供通用的AssetBundles设计模式。</p>
<p>在决定如何将对象分组到AssetBundles时，如果必须同时加载或更新对象，则通常最好将对象捆绑到AssetBundles中。例如，考虑一个角色扮演游戏。个别的地图和裁剪场景可以按场景分组成AssetBundles，但有些对象可能会存在于很多其它场景则不能划分进去。AssetBundles可以用来提供肖像画，游戏中的UI，以及不同的角色模型和纹理。这些稍后需要的Objects和Assets可以分组到第二组AssetBundles中，这些Assets在启动时加载，并在应用程序的生命周期内保持加载状态。</p>
<p>另一个问题可能会出现，如果Unity必须在AssetBundle卸载后从AssetBundle中重新加载一个对象。在这种情况下，重新加载将失败，该对象将以(Missing)对象的形式出现在Unity编辑器的层次结构中。</p>
<p>这主要会发生在Unity失去并试图恢复对其图形上下文的控制时，例如当移动应用程序被挂起或用户锁定他们的PC时。在这种情况下，Unity必须重新上传纹理和渲染到GPU。如果这些Asset的源AssetBundle不可用，则应用程序会将场景中的相关对象呈现为洋红色。</p>
<h2 id="发布"><a href="#发布" class="headerlink" title="发布"></a>发布</h2><p>客户端发布项目的AssetBundles有两种基本方法：和项目一起安装或安装后再下载它们。</p>
<p>在安装时还是安装后交付AssetBundles，取决于项目将运行的平台的功能和限制。移动项目通常选择先安装后下载，以减少初始安装大小，并保持低于相关平台下载的大小限制（比如苹果商店和谷歌商店会对4G模式下最大能下载的包做限制）。控制台和PC项目通常在初始安装时附带AssetBundles。</p>
<p>良好的架构应该允许项目在安装后再进行资源热更，而不用管AssetBundles最初是如何发布的。有关这方面的更多信息，请参见“<a target="_blank" rel="noopener" href="https://docs.unity3d.com/Manual/AssetBundles-Patching.html?_ga=2.161080382.2009813986.1571904029-1030292064.1564583003">Unity 手册</a>”中的“Patching with AssetBundles”一节。</p>
<h3 id="随项目发布"><a href="#随项目发布" class="headerlink" title="随项目发布"></a>随项目发布</h3><p>与项目一起发布AssetBundles是最简单的，因为它不需要额外的代码进行下载和管理。项目在安装时包含AssetBundles，有两个主要原因：</p>
<ul>
<li>减少项目构建时间并允许更简单的迭代开发。如果这些AssetBundles不需要和应用程序本身分开更新，可以通过将AssetBundles存储在Streaming Assets中，将AssetBundles包含在应用程序中。请参阅下面的 Streaming Assets部分。</li>
<li>发布可更新内容的初始修订版。这通常是为了节省终端用户在最初安装后的时间，或者作为以后修补的基础。Streaming Assets在这种情况下并不理想。但是，如果不选择编写自定义下载和缓存系统，则可以从Streaming Assets将可更新内容的初始修订加载到Unity缓存中（请参阅下面的缓存启动部分）。</li>
</ul>
<h3 id="安装后下载"><a href="#安装后下载" class="headerlink" title="安装后下载"></a>安装后下载</h3><p>将AssetBundles传递到移动设备的最好方法是在应用程序安装后再下载它们。这就允许在安装后再更新游戏内容，而不必强迫用户重新下载整个应用程序。在许多平台上，应用程序二进制文件必须经过昂贵而漫长的重新认证过程。因此，开发一个良好的分离下载系统是至关重要的。</p>
<p>交付AssetBundles最简单的方法是将它们放在某个Web服务器上，并通过UnityWebRequest发布。Unity将自动在本地存储上缓存下载的AssetBundles。如果下载的AssetBundle是LZMA压缩的，那么AssetBundle将以未压缩或重新压缩的形式存储在缓存中，就像LZ 4一样（依赖<code>Caching.compressionEnabled</code>设置），以便将来更快地加载。如果下载的包是LZ 4压缩的，AssetBundles将被压缩存储。如果缓存被填满，Unity将从缓存中删除最近使用最少的AssetBundle。</p>
<p>通常建议在允许的情况下使用UnityWebRequest，或者只有在使用Unity 5.2或更老版本时才使用<code>WWW.LoadFromCacheOrDownload</code>。只有当内置API的内存消耗、缓存行为或性能对于特定项目是不可接受的时候，或者项目必须运行特定于平台的代码才能满足其需求时，才需要对自定义下载系统进行扩展。</p>
<p>可能会阻碍使用UnityWebRequest或<code>WWW.LoadFromCacheOrDownload</code>的情况示例：</p>
<ul>
<li>当需要对AssetBundle缓存进行颗粒度控制时。</li>
<li>当项目需要实现自定义压缩策略时。</li>
<li>当项目希望使用特定于平台的API来满足某些需求时，例如在不活动时才加载流数据。比如，使用IOS的后台任务API下载数据。</li>
<li>当AssetBundles必须在不具备SSL支持条件(如PC)的平台上通过SSL交付时时候。</li>
</ul>
<h3 id="建立缓存"><a href="#建立缓存" class="headerlink" title="建立缓存"></a>建立缓存</h3><p>Unity有一个内置的AssetBundle缓存系统，可以用来缓存通过UnityWebRequest API下载的AssetBundles，该API的重载会接受一个AssetBundle版本号作为参数。这个数字不是存储在AssetBundles里的，也不是由AssetBundles系统生成的。</p>
<p>缓存系统跟踪传递给UnityWebRequest的最后一个版本号。当使用版本号调用此API时，缓存系统通过比较版本号来检查是否存在缓存的AssetBundle。如果这些数字匹配，系统将加载缓存的AssetBundle。如果数字不匹配，或者没有缓存的AssetBundle，那么Unity将下载一个新的副本。此新副本将与新的版本号相关联。</p>
<p><strong>缓存系统中的AssetBundle只通过它们的文件名来标识</strong>，而不是通过下载它们的完整URL。这意味着具有相同文件名的AssetBundle可以存储在多个不同的位置，例如CDN。只要文件名相同，缓存系统就会将它们识别为相同的AssetBundle。</p>
<p>每个应用程序都需要确定将版本号分给AssetBundles的考量，然后将这些编号传递给UnityWebRequest。数字可能来自某些唯一标识符，例如crc值。请注意，虽然<code>AssetBundleManifest.GetAssetBundleHash()</code>也可用于此目的，但我们不建议将此函数用于版本控制，因为它只提供了估计，而不是真正的哈希计算。</p>
<h1 id="AssetBundle编译过程"><a href="#AssetBundle编译过程" class="headerlink" title="AssetBundle编译过程"></a>AssetBundle编译过程</h1><h2 id="MenuItem"><a href="#MenuItem" class="headerlink" title="MenuItem"></a>MenuItem</h2><p>通过调用MenuItem对应的打包函数，对应到AssetBundle下的Build方法，实质上执行的是BuildAssetBundleStage重写了父类IStage的Process方法，主要通过以下步骤启动AB打包：<br><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">RestoreCombinedNameIndexPool();</span><br><span class="line">RestoreOutGameCombinedNameIndexPool();</span><br><span class="line">Build();</span><br><span class="line">LinkAssetEntriesToBundleIds();</span><br><span class="line">LinkLevelScenesToBundleIds();</span><br></pre></td></tr></table></figure><br>其中主要还是<code>Build()</code>函数是核心，其他的无非是在做初始化和资源链接</p>
<h2 id="Build"><a href="#Build" class="headerlink" title="Build"></a>Build</h2><p>因为魂斗罗项目比较特殊，项目组在开发工程里就把所有资源全放进了Resource目录，所以在AB构建的时候需要不断建立一些链接来提取需要构建进AB包的资源。除了前面的步骤外，<code>Build()</code>函数内也有例如以下的链接函数：</p>
<figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">LinkGameModeToCommonBundleIds();</span><br><span class="line">LinkAssetEntryToCommonBundleIds();</span><br><span class="line">LinkControllerToHallModel();</span><br></pre></td></tr></table></figure>
<p>除此之外，还可以用<code>AssetBundleStatisticsHelper.RecordPackedSize();</code>记录打包的大小。然后就是关键的调用引擎的AsseBundle构建函数了：</p>
<figure class="highlight cs"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//如果内存不够的话，统一构建AssetBundle会报out of memory</span></span><br><span class="line">BuildPipeline.BuildAssetBundles(exportPath, buildMap,</span><br><span class="line">    buildOptions,</span><br><span class="line">    EditorUserBuildSettings.activeBuildTarget);</span><br><span class="line"><span class="comment">// 因此采用下面逐个构建的方式，采用下面的方式buildOptions需要加上DontCompile</span></span><br><span class="line"><span class="comment">// Unity2017之后没有DontCompile的选项了，就不用管了</span></span><br><span class="line"><span class="comment">// foreach (var bundle in buildMap)</span></span><br><span class="line"><span class="comment">// &#123;</span></span><br><span class="line"><span class="comment">//      BuildPipeline.BuildAssetBundles(exportPath,</span></span><br><span class="line"><span class="comment">//         new AssetBundleBuild[] &#123; bundle, &#125;,</span></span><br><span class="line"><span class="comment">//         buildOptions,</span></span><br><span class="line"><span class="comment">//         EditorUserBuildSettings.activeBuildTarget);</span></span><br><span class="line"><span class="comment">// &#125;</span></span><br></pre></td></tr></table></figure>
<p>函数的最后还会执行一个<code>GenExtBundleInfo</code>的函数，主要用于生成AB包的MD5，在后续步骤中进行校验</p>
<h2 id="BuildAssetBundles"><a href="#BuildAssetBundles" class="headerlink" title="BuildAssetBundles"></a>BuildAssetBundles</h2><p>由<code>public static AssetBundleManifest BuildAssetBundles</code>实际通过指定平台后的<code>internal</code>同名方法再调用<code>BuildAssetBundlesInternal</code>启动AB构建的，这个方法在<code>Editor\Mono\BuildPipeline.bindings.cs</code>中只做了extern的外部声明，实际实现在<code>Editor\Src\BuildPipeline\BuildAssetBundle.cpp</code>中的静态方法。</p>
<h3 id="BuildAssetBundlesInternal"><a href="#BuildAssetBundlesInternal" class="headerlink" title="BuildAssetBundlesInternal"></a>BuildAssetBundlesInternal</h3><p>AB构建的根入口，每一个异常终止的分支都会调用<code>SendBuildAssetBundlesAnalytics</code>发送错误报告并返回<code>NULL</code></p>
<ol>
<li>判断是否支持该平台的构建，不支持就异常处理</li>
<li>因为不支持在Unity运行态时build AB，所以需要先终止，然后这个方法里会用<code>SetApplicationIsPlaying</code>相当于抢占锁了</li>
<li>做一些例如License、Variants、BuildOptions是否存在且合法的判断，一样是不合法就异常处理</li>
<li>保存previous平台的一些信息，保证build完AB之后可以顺利切换当前平台（如果需要的话）</li>
<li>通过<code>CalculateAssetBundlesToBeBuilt</code>这个<strong>比较复杂的函数</strong>提取需要构建的AB资源</li>
<li>通过<code>BuildAssetBundleHelper.cpp</code>中的<code>CheckSceneAssetBundles</code>方法检查并标记场景文件</li>
<li>通过之前随AB包打出来的Hash对比现在的预打包的AB包，大小不一就说明有变化需要重新build，同时赋值一些例如压缩方法、额外流式场景之类的buildOptions</li>
<li>判断<code>GenerateAssemblyTypeInfos</code>能否正确返回，否则异常处理之</li>
<li>预编译一些例如质量设置等的配置项，注意这里维护了一个<code>m_ManagersToReset</code>列表，用<code>push_back</code>和<code>back</code>方法取新的当前对象。这种列表的维护理想在引擎的很多地方都存在，例如协程等需要记录上下文的场景</li>
<li>（核心构建入口）对AB资源的各个子序列调用<code>BuildAssetBundle</code>开始构建，收集完asset内所有对象后开始调用同名的静态函数进行Build，详见下文子章节</li>
<li>用<code>core::hash_map&lt;SInt32, AssetDatabase::AssetBundleFullName&gt;</code>按照AB资源名称进行缓存，避免<code>AssetDatabase::GetAssetBundleName</code>的多次调用，那样会降低效率</li>
<li><p>确定这个scene asset放在正确位置后，判断是否是最后一个asset bundle。如果是最后一个可以用后处理机制来优化，避免不必要的重复编译脚本。当然，出现异常Abort也走这个后处理：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">    <span class="built_in">RestoreBuildTarget</span>(previousTargetGroup, previousTargetPlatform, previousSubtarget);</span><br><span class="line">    <span class="built_in">CleanupAfterBuildPlayer</span>(kPlayerAssembliesPath);</span><br><span class="line">    report.<span class="built_in">SetBuildResult</span>(BuildReporting::kResultFailed);</span><br><span class="line">    report.<span class="built_in">EndBuildStep</span>(buildStepBundleToken);</span><br><span class="line">    <span class="built_in">EndPlayerBuilding</span>(report, sceneBackups, buildStepAllSceneBundles);</span><br><span class="line">    <span class="built_in">CleanupAssetBundleBuild</span>();</span><br><span class="line">    <span class="built_in">SendBuildAssetBundlesAnalytics</span>(<span class="built_in">tr</span>(<span class="string">&quot;Aborting Build Due to Errors&quot;</span>));</span><br></pre></td></tr></table></figure>
</li>
<li><p>每次循环中<code>MonitoredGarbageCollectIfHighMemoryUsage</code>的<code>UnloadUnusedAssetsImmediate</code>可以动态卸载不需要的assts来节省内存，同时监控GC后的内存变化。</p>
</li>
<li>调用<code>CleanupAfterBuildPlayer</code>，恢复配置到未指定平台</li>
</ol>
<h3 id="BuildAssetBundle"><a href="#BuildAssetBundle" class="headerlink" title="BuildAssetBundle"></a>BuildAssetBundle</h3><p>核心的AssetBundle迭代构建入口</p>
<ol>
<li><code>report.SetBuildResult(BuildReporting::kResultFailed);</code>先假设失败，看来作者是悲观派</li>
<li>根据设置开始创建各种需要的文件夹，包括存放AB的、inernalPath、临时路径</li>
<li>随后为packaging步骤预备bundle文件，但实际上暂时并未加载到内存</li>
<li><p>在运行<code>CalculateBuildCompressionSettingsFromLegacyBuildAssetBundleOptions</code>后调用<code>BuildAssetBundleArchiveFile</code>编译archive文件，这一个函数内：</p>
<ol>
<li>给scene根据index排序，准备编译header的配置以及压缩设置（我们一般用LZ4）</li>
<li><p>然后一个场景一个场景添加到archive</p>
<blockquote>
<p>在此过程中，我们访问了前面申明的内存区间中的地址，如果尝试访问内存地址出现异常，大概率就是这里分配出来的内存被占用/被释放/超出机器极限。可以先尝试设置4倍的虚拟内存再尝试，虚拟内存再大效果可能会变差，需要过多CPU时间去定位映射出来的虚拟内存地址。</p>
</blockquote>
</li>
<li><p>做一些文件迁移出临时文件夹、文件重定位、大小统计的活</p>
</li>
</ol>
</li>
</ol>
 
      <!-- reward -->
      
      <div id="reword-out">
        <div id="reward-btn">
          打赏
        </div>
      </div>
      
    </div>
    

    <!-- copyright -->
    
    <div class="declare">
      <ul class="post-copyright">
        <li>
          <i class="ri-copyright-line"></i>
          <strong>版权声明： </strong>
          
          本博客所有文章除特别声明外，著作权归作者所有。转载请注明出处！
          
        </li>
      </ul>
    </div>
    
    <footer class="article-footer">
       
<div class="share-btn">
      <span class="share-sns share-outer">
        <i class="ri-share-forward-line"></i>
        分享
      </span>
      <div class="share-wrap">
        <i class="arrow"></i>
        <div class="share-icons">
          
          <a class="weibo share-sns" href="javascript:;" data-type="weibo">
            <i class="ri-weibo-fill"></i>
          </a>
          <a class="weixin share-sns wxFab" href="javascript:;" data-type="weixin">
            <i class="ri-wechat-fill"></i>
          </a>
          <a class="qq share-sns" href="javascript:;" data-type="qq">
            <i class="ri-qq-fill"></i>
          </a>
          <a class="douban share-sns" href="javascript:;" data-type="douban">
            <i class="ri-douban-line"></i>
          </a>
          <!-- <a class="qzone share-sns" href="javascript:;" data-type="qzone">
            <i class="icon icon-qzone"></i>
          </a> -->
          
          <a class="facebook share-sns" href="javascript:;" data-type="facebook">
            <i class="ri-facebook-circle-fill"></i>
          </a>
          <a class="twitter share-sns" href="javascript:;" data-type="twitter">
            <i class="ri-twitter-fill"></i>
          </a>
          <a class="google share-sns" href="javascript:;" data-type="google">
            <i class="ri-google-fill"></i>
          </a>
        </div>
      </div>
</div>

<div class="wx-share-modal">
    <a class="modal-close" href="javascript:;"><i class="ri-close-circle-line"></i></a>
    <p>扫一扫，分享到微信</p>
    <div class="wx-qrcode">
      <img src="//api.qrserver.com/v1/create-qr-code/?size=150x150&data=https://blog.shimmerjordan.eu.org/2024/08/23/assetbundle-intro/" alt="微信分享二维码">
    </div>
</div>

<div id="share-mask"></div>  
  <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Assetbundle/" rel="tag">Assetbundle</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/Unity/" rel="tag">Unity</a></li></ul>

    </footer>
  </div>

   
  <nav class="article-nav">
    
    
      <a href="/2024/08/22/unity-asset-resources/" class="article-nav-link">
        <strong class="article-nav-caption">下一篇</strong>
        <div class="article-nav-title">Unity资源映射</div>
      </a>
    
  </nav>

   
<!-- valine评论 -->
<div id="vcomments-box">
  <div id="vcomments"></div>
</div>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/valine@1.4.14/dist/Valine.min.js"></script>
<script>
  new Valine({
    el: "#vcomments",
    app_id: "StYfTMDp78X0EltFR16ve2q5-gzGzoHsz",
    app_key: "G4RPxRpXG6RwdfpnJefOSnyy",
    path: window.location.pathname,
    avatar: "wavatar",
    placeholder: "ヾﾉ≧∀≦)o来啊，快活啊!",
    recordIP: true,
  });
  const infoEle = document.querySelector("#vcomments .info");
  if (infoEle && infoEle.childNodes && infoEle.childNodes.length > 0) {
    infoEle.childNodes.forEach(function (item) {
      item.parentNode.removeChild(item);
    });
  }
</script>
<style>
  #vcomments-box {
    padding: 5px 30px;
  }

  @media screen and (max-width: 800px) {
    #vcomments-box {
      padding: 5px 0px;
    }
  }

  #vcomments-box #vcomments {
    background-color: #fff;
  }

  .v .vlist .vcard .vh {
    padding-right: 20px;
  }

  .v .vlist .vcard {
    padding-left: 10px;
  }
</style>

 
   
   
<!-- minivaline评论 -->
<div id="mvcomments-box">
  <div id="mvcomments"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/minivaline@latest"></script>
<script>
    new MiniValine(Object.assign({"enable":true,"mode":"DesertsP","placeholder":"Write a Comment","math":true,"md":true,"enableQQ":true,"NoRecordIP":false,"visitor":true,"maxNest":6,"pageSize":6,"adminEmailMd5":"de8a7aa53d07e6b6bceb45c64027763d","tagMeta":["管理员","小伙伴","访客"],"master":["de8a7aa53d07e6b6bceb45c64027763d"],"friends":["b5bd5d836c7a0091aa8473e79ed4c25e","adb7d1cd192658a55c0ad22a3309cecf","3ce1e6c77b4910f1871106cb30dc62b0","cfce8dc43725cc14ffcd9fb4892d5bfc"],"lang":null,"emoticonUrl":["https://cdn.jsdelivr.net/npm/alus@latest","https://cdn.jsdelivr.net/gh/MiniValine/qq@latest","https://cdn.jsdelivr.net/gh/MiniValine/Bilibilis@latest","https://cdn.jsdelivr.net/gh/MiniValine/tieba@latest","https://cdn.jsdelivr.net/gh/MiniValine/twemoji@latest","https://cdn.jsdelivr.net/gh/MiniValine/weibo@latest"]}, {
	  el: '#mvcomments',
    }));
  const infoEle = document.querySelector('#mvcomments .info');
  if (infoEle && infoEle.childNodes && infoEle.childNodes.length > 0) {
      infoEle.childNodes.forEach(function (item) {
          item.parentNode.removeChild(item);
      });
  }
</script>
<style>
	#mvcomments-box {
		padding: 5px 30px;
	}
	@media screen and (max-width: 800px) {
		#mvcomments-box {
		  padding: 5px 0px;
		}
	}
	.darkmode .MiniValine *{
		color: #f1f1f1!important;
	}
	.darkmode .commentTrigger{
		background-color: #403e3e !important;
	  }
	.darkmode .MiniValine .vpage .more{
		background: #21232F
	}
	.darkmode img{
		filter: brightness(30%)
	}
	.darkmode .MiniValine .vlist .vcard .vcomment-body .text-wrapper .vcomment.expand:before{
		background: linear-gradient(180deg, rgba(246,246,246,0), rgba(0,0,0,0.9))
	}
	.darkmode .MiniValine .vlist .vcard .vcomment-body .text-wrapper .vcomment.expand:after{
		background: rgba(0,0,0,0.9)
	}
	.darkmode .MiniValine .vlist .vcard .vcomment-body .text-wrapper .vcomment pre{
		background: #282c34
		border: 1px solid #282c34
	}
	.darkmode .MiniValine .vinputs-area .textarea-wrapper textarea{
		color: #000;
	}
	.darkmode .MiniValine .vinputs-area .auth-section .input-wrapper input{
		color: #000;
	}
	.darkmode .MiniValine .vinputs-area .vextra-area .vsmile-icons{
		background: transparent;
	}
	.darkmode .MiniValine .vinputs-wrap{
		border-color: #b2b2b5;
	}
	.darkmode .MiniValine .vinputs-wrap:hover{
		border: 1px dashed #2196f3;
	}
	.darkmode .MiniValine .vinputs-area .auth-section .input-wrapper{
		border-bottom: 1px dashed #b2b2b5;
	}
	.darkmode .MiniValine .vinputs-area .auth-section .input-wrapper:hover{
		border-bottom: 1px dashed #2196f3;
	}
	.darkmode .MiniValine .vbtn{
		background-color: transparent!important;
	}
	.darkmode .MiniValine .vbtn:hover{
		border: 1px dashed #2196f3;
	}
</style>

    
</article>

</section>
      <footer class="footer">
  <div class="outer">
    <ul>
      <li>
        Copyrights &copy;
        2019-2024
        <i class="ri-heart-fill heart_icon"></i> 鞠桥丹-QIAODAN JU
      </li>
    </ul>
    <ul>
      <li>
        
        
        
        由 <a href="https://hexo.io" target="_blank">Hexo</a> 强力驱动
        <span class="division">|</span>
        主题 - <a href="https://github.com/Shen-Yu/hexo-theme-ayer" target="_blank">Ayer</a>
        
      </li>
    </ul>
    <ul>
      <li>
        
        
        <span>
  <span><i class="ri-user-3-fill"></i>访问人数:<span id="busuanzi_value_site_uv"></span></s>
  <span class="division">|</span>
  <span><i class="ri-eye-fill"></i>浏览次数:<span id="busuanzi_value_page_pv"></span></span>
</span>
        
      </li>
    </ul>
    <ul>
      
    </ul>
    <ul>
      
    </ul>
    <ul>
      <li>
        <!-- cnzz统计 -->
        
        <script type="text/javascript" src='https://s4.cnzz.com/z_stat.php?id=1279035150&amp;web_id=1279035150'></script>
        
      </li>
    </ul>
  </div>
</footer>
      <div class="float_btns">
        <div class="totop" id="totop">
  <i class="ri-arrow-up-line"></i>
</div>

<div class="todark" id="todark">
  <i class="ri-moon-line"></i>
</div>

      </div>
    </main>
    <aside class="sidebar on">
      <button class="navbar-toggle"></button>
<nav class="navbar">
  
  <div class="logo">
    <a href="/"><img src="/images/ayer-side.svg" alt="丛烨-shimmerjordan"></a>
  </div>
  
  <ul class="nav nav-main">
    
    <li class="nav-item">
      <a class="nav-item-link" href="/">Home</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/archives">Catalogue</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags">Tags</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/tags/%E9%9A%8F%E7%AC%94/">Essay</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/categories">Archives</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/friends">Friends</a>
    </li>
    
    <li class="nav-item">
      <a class="nav-item-link" href="/2020/01/18/about">About</a>
    </li>
    
  </ul>
</nav>
<nav class="navbar navbar-bottom">
  <ul class="nav">
    <li class="nav-item">
      
      <a class="nav-item-link nav-item-search"  title="搜索">
        <i class="ri-search-line"></i>
      </a>
      
      
      <a class="nav-item-link" target="_blank" href="/atom.xml" title="RSS Feed">
        <i class="ri-rss-line"></i>
      </a>
      
    </li>
  </ul>
</nav>
<div class="search-form-wrap">
  <div class="local-search local-search-plugin">
  <input type="search" id="local-search-input" class="local-search-input" placeholder="Search...">
  <div id="local-search-result" class="local-search-result"></div>
</div>
</div>
    </aside>
    <script>
      if (window.matchMedia("(max-width: 768px)").matches) {
        document.querySelector('.content').classList.remove('on');
        document.querySelector('.sidebar').classList.remove('on');
      }
    </script>
    <div id="mask"></div>

<!-- #reward -->
<div id="reward">
  <span class="close"><i class="ri-close-line"></i></span>
  <p class="reward-p"><i class="ri-cup-line"></i>请我喝杯蓝莓汁吧~</p>
  <div class="reward-box">
    
    <div class="reward-item">
      <img class="reward-img" src="/images/alipay.jpg">
      <span class="reward-type">支付宝</span>
    </div>
    
    
    <div class="reward-item">
      <img class="reward-img" src="/images/wechat.jpg">
      <span class="reward-type">微信</span>
    </div>
    
  </div>
</div>
    
<script src="/js/jquery-2.0.3.min.js"></script>


<script src="/js/lazyload.min.js"></script>

<!-- Tocbot -->


<script src="/js/tocbot.min.js"></script>

<script>
  tocbot.init({
    tocSelector: '.tocbot',
    contentSelector: '.article-entry',
    headingSelector: 'h1, h2, h3, h4, h5, h6',
    hasInnerContainers: true,
    scrollSmooth: true,
    scrollContainer: 'main',
    positionFixedSelector: '.tocbot',
    positionFixedClass: 'is-position-fixed',
    fixedSidebarOffset: 'auto'
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-modal@0.9.2/jquery.modal.min.css">
<script src="https://cdn.jsdelivr.net/npm/justifiedGallery@3.7.0/dist/js/jquery.justifiedGallery.min.js"></script>

<script src="/dist/main.js"></script>

<!-- ImageViewer -->

<!-- Root element of PhotoSwipe. Must have class pswp. -->
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

    <!-- Background of PhotoSwipe. 
         It's a separate element as animating opacity is faster than rgba(). -->
    <div class="pswp__bg"></div>

    <!-- Slides wrapper with overflow:hidden. -->
    <div class="pswp__scroll-wrap">

        <!-- Container that holds slides. 
            PhotoSwipe keeps only 3 of them in the DOM to save memory.
            Don't modify these 3 pswp__item elements, data is added later on. -->
        <div class="pswp__container">
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
            <div class="pswp__item"></div>
        </div>

        <!-- Default (PhotoSwipeUI_Default) interface on top of sliding area. Can be changed. -->
        <div class="pswp__ui pswp__ui--hidden">

            <div class="pswp__top-bar">

                <!--  Controls are self-explanatory. Order can be changed. -->

                <div class="pswp__counter"></div>

                <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>

                <button class="pswp__button pswp__button--share" style="display:none" title="Share"></button>

                <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>

                <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>

                <!-- Preloader demo http://codepen.io/dimsemenov/pen/yyBWoR -->
                <!-- element will get class pswp__preloader--active when preloader is running -->
                <div class="pswp__preloader">
                    <div class="pswp__preloader__icn">
                        <div class="pswp__preloader__cut">
                            <div class="pswp__preloader__donut"></div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
                <div class="pswp__share-tooltip"></div>
            </div>

            <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
            </button>

            <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
            </button>

            <div class="pswp__caption">
                <div class="pswp__caption__center"></div>
            </div>

        </div>

    </div>

</div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css">
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"></script>

<script>
    function viewer_init() {
        let pswpElement = document.querySelectorAll('.pswp')[0];
        let $imgArr = document.querySelectorAll(('.article-entry img:not(.reward-img)'))

        $imgArr.forEach(($em, i) => {
            $em.onclick = () => {
                // slider展开状态
                // todo: 这样不好，后面改成状态
                if (document.querySelector('.left-col.show')) return
                let items = []
                $imgArr.forEach(($em2, i2) => {
                    let img = $em2.getAttribute('data-idx', i2)
                    let src = $em2.getAttribute('data-target') || $em2.getAttribute('src')
                    let title = $em2.getAttribute('alt')
                    // 获得原图尺寸
                    const image = new Image()
                    image.src = src
                    items.push({
                        src: src,
                        w: image.width || $em2.width,
                        h: image.height || $em2.height,
                        title: title
                    })
                })
                var gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, {
                    index: parseInt(i)
                });
                gallery.init()
            }
        })
    }
    viewer_init()
</script>

<!-- MathJax -->

<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
      tex2jax: {
          inlineMath: [ ['$','$'], ["\\(","\\)"]  ],
          processEscapes: true,
          skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
      }
  });

  MathJax.Hub.Queue(function() {
      var all = MathJax.Hub.getAllJax(), i;
      for(i=0; i < all.length; i += 1) {
          all[i].SourceElement().parentNode.className += ' has-jax';
      }
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.6/unpacked/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
  var ayerConfig = {
    mathjax: true
  }
</script>

<!-- Katex -->

<!-- busuanzi  -->


<script src="/js/busuanzi-2.3.pure.min.js"></script>


<!-- ClickLove -->


<script src="/js/clickLove.js"></script>


<!-- ClickBoom1 -->

<!-- ClickBoom2 -->

<!-- CodeCopy -->


<link rel="stylesheet" href="/css/clipboard.css">

<script src="https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js"></script>
<script>
  function wait(callback, seconds) {
    var timelag = null;
    timelag = window.setTimeout(callback, seconds);
  }
  !function (e, t, a) {
    var initCopyCode = function(){
      var copyHtml = '';
      copyHtml += '<button class="btn-copy" data-clipboard-snippet="">';
      copyHtml += '<i class="ri-file-copy-2-line"></i><span>COPY</span>';
      copyHtml += '</button>';
      $(".highlight .code pre").before(copyHtml);
      $(".article pre code").before(copyHtml);
      var clipboard = new ClipboardJS('.btn-copy', {
        target: function(trigger) {
          return trigger.nextElementSibling;
        }
      });
      clipboard.on('success', function(e) {
        let $btn = $(e.trigger);
        $btn.addClass('copied');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-checkbox-circle-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPIED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-checkbox-circle-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
      clipboard.on('error', function(e) {
        e.clearSelection();
        let $btn = $(e.trigger);
        $btn.addClass('copy-failed');
        let $icon = $($btn.find('i'));
        $icon.removeClass('ri-file-copy-2-line');
        $icon.addClass('ri-time-line');
        let $span = $($btn.find('span'));
        $span[0].innerText = 'COPY FAILED';
        
        wait(function () { // 等待两秒钟后恢复
          $icon.removeClass('ri-time-line');
          $icon.addClass('ri-file-copy-2-line');
          $span[0].innerText = 'COPY';
        }, 2000);
      });
    }
    initCopyCode();
  }(window, document);
</script>


<!-- CanvasBackground -->


<script src="/js/dz.js"></script>



    
  </div>
</body>

</html>